package com.carsa.blackberry.util;

/**
 * @author Nahuel Barrios.
 * @since 28/07/2010.
 * @version 1.0
 */

public class XMLHandler implements XMLPortable {
    protected static final String LT_STRING     = "<";
    protected static final String GT_STRING     = ">";
    protected static final char   GT_CHAR       = '>';
    protected static final char   SPACE_CHAR    = ' ';
    protected static final String START_END_TAG = "</";

    public static final String    ROOT_PATH     = "/";

    private String                xml;
    private String                buffer;
    private String                rootTagName;
    private String                xmlVersionTag;

    /**
     * Se instancia un nuevo XMLHandler para crear un XML.
     * 
     * @param rootTagName
     * @param attributes
     * @param initialValue
     */
    public XMLHandler(String rootTagName, String[] attributes, String initialValue) {
	super();
	this.rootTagName = rootTagName;
	this.xml = this.getCompleteStartTag(rootTagName, attributes);
	if (initialValue != null)
	    this.xml += initialValue;

	this.xml += this.getEndTag(this.rootTagName);
    }

    /**
     * @param xml
     *            The xml data to handle.
     */
    public XMLHandler(String xml) {
	super();
	this.initializeFromXML(xml);
    }

    public XMLHandler() {
	super();
    }

    /**
     * Setea el {@link String} en formato XML que se va a manejar.
     * 
     * @param xml
     *            El XML.
     */
    public void setXml(String xml) {
	this.xml = xml;
    }

    /**
     * @param elementName
     *            Un {@link String} con el nombre del elemento que queremos obtener.
     * @return Un {@link String} en formato XML con el elemento indicado en elementName. Algo similar a:
     *         <elementName>elementValue</elementName>
     */
    public String getElement(String elementName) {
	int[] indexes = this.getElementIndexes(elementName);

	if (!this.checkIndexes(indexes))
	    return null;

	return this.xml.substring(indexes[0], indexes[1]);
    }

    /**
     * @param elementName
     *            Un {@link String} con el nombre del elemento del cual queremos obtener su valor.
     * @return Un {@link String} con el valor del elemento XML indicado. Si el elemento es:
     *         <elementName>elementValue</elementName> entonces esta funcion retornaria: elementValue. Nota: Si el
     *         elemento no existiera, entonces retorna <code>null</code>.
     */
    public String getElementValue(String elementName) {
	int[] indexes = this.getElementValueIndexes(elementName);

	if (!this.checkIndexes(indexes))
	    return null;

	return this.xml.substring(indexes[0], indexes[1]);
    }

    private boolean checkIndexes(int[] indexes) {
	for (int idx = 0; idx < indexes.length; idx++) {
	    if (indexes[idx] < 0)
		return false;
	}
	return true;
    }

    /**
     * @return Un {@link String} con el nombre del primer elemento del XML.
     */
    public String getFirstElementTagName() {
	// Obtengo el indice del primer espacio y del primer mayor que,
	// dado que el XML seria:
	// <nombreDelElemento>valor</nombreDelElemento>
	// o tambien:
	// <nombreDelElemento unAtributo="suValor">valor</nombreDelElemento>

	int indexOfSpace = this.xml.indexOf(SPACE_CHAR);
	int indexOfGT = this.xml.indexOf(GT_CHAR);

	// El substring lo hago desde 1 para que no entre el primer '<'
	if (this.firstElementHasAttributes())
	    return this.xml.substring(1, indexOfSpace);
	else
	    return this.xml.substring(1, indexOfGT);
    }

    /**
     * @return Un {@link String} con el valor del primer elemento del XML.
     */
    public String getFirstElementValue() {
	return this.getElementValue(this.getFirstElementTagName());
    }

    /**
     * @return Un {@link String} con el valor del primer elemento del XML.
     */
    public String getFirstElementValueOverIteration() {
	String firstTagName = this.getFirstElementTagName();
	String value = this.getElementValue(firstTagName);

	String endTag = this.getEndTag(firstTagName);
	int breakpoint = this.xml.indexOf(endTag) + endTag.length();

	this.xml = this.xml.substring(breakpoint);

	return value;
    }

    /**
     * @param tagName
     *            Un {@link String} con el nombre del elemento que queremos buscar.
     * @return Un {@link String} conteniendo <+tagName para poder buscar su indice en el XML.
     */
    private String getStartTagForSearch(String tagName) {
	return LT_STRING + tagName;
    }

    /**
     * @param tagName
     *            El nombre del tag que queremos armar.
     * @return Un {@link String} conteniendo <tagName>
     */
    private String getEndTag(String tagName) {
	return START_END_TAG + tagName + GT_STRING;
    }

    /**
     * @param elementName
     *            El nombre del elemento del cual queremos averiguar sus indices.
     * @return Un array de int de dos posiciones donde la posicion cero contiene el comienzo del elemento elementName
     *         (incluyendo los tags XML de apertura y cierre del elemento), y donde la posicion uno cnotiene el final.
     */
    private int[] getElementIndexes(String elementName) {
	int[] ret = new int[2];

	int startTagIdx = this.xml.indexOf(this.getStartTagForSearch(elementName));
	ret[0] = startTagIdx;

	String endTag = this.getEndTag(elementName);
	int endTagIdx = this.xml.indexOf(endTag) + endTag.length();
	ret[1] = endTagIdx;

	return ret;
    }

    /**
     * @param elementName
     *            El nombre del elemento del cual queremos averiguar sus indices.
     * @return Un array de int de dos posiciones donde la posicion cero contiene el comienzo del elemento elementName
     *         (sin incluir los tags XML de apertura y cierre del elemento), y donde la posicion uno cnotiene el final.
     */
    private int[] getElementValueIndexes(String elementName) {
	int[] ret = new int[2];

	try {
	    int startTagIdx = this.xml.indexOf(this.getStartTagForSearch(elementName));
	    String tmp = this.xml.substring(startTagIdx);
	    ret[0] = startTagIdx + tmp.indexOf(GT_CHAR) + 1;// +1 asi no entra
	    // el ">"
	}
	catch (Exception e) {
	    Logger.logError(Logger.getLocationMessageFor(this, "getElementValueIndexes(...)"), e);
	}

	String endTag = this.getEndTag(elementName);
	ret[1] = this.xml.indexOf(endTag);
	return ret;
    }

    // public String getElementsForIteration(String parentElementName, String
    // childrenName){
    //		
    // Hashtable table= new Hashtable();
    //		
    // XMLHandler handler= new XMLHandler(this.xml);
    // handler= new XMLHandler(handler.getElement(parentElementName));
    //		
    //		
    // Vector vector
    // int[] indexes = this.getElementIndexes(childrenName);
    // this.xml.substring(indexes[0], indexes[1]);
    //		
    // return null;
    // }

    private boolean firstElementHasAttributes() {
	// Obtengo el indice del primer espacio y del primer mayor que,
	// dado que el XML seria:
	// <nombreDelElemento>valor</nombreDelElemento>
	// o tambien:
	// <nombreDelElemento unAtributo="suValor">valor</nombreDelElemento>

	int indexOfSpace = this.xml.indexOf(SPACE_CHAR);
	int indexOfGT = this.xml.indexOf(GT_CHAR);

	return indexOfSpace > 0 && indexOfSpace < indexOfGT;

    }

    // /**
    // *
    // * @param elementName
    // * @param elementValue
    // * @return
    // */
    // public boolean addElementAtRoot(String elementName, String elementValue)
    // {
    // return this.addElement(ROOT_PATH, elementName, null, elementValue);
    // }
    //	
    // /**
    // *
    // * @param elementName
    // * @param attributes
    // * @param elementValue
    // * @return
    // */
    // public boolean addElementAtRoot(String elementName, String[] attributes,
    // String elementValue) {
    // return this.addElement(ROOT_PATH, elementName, attributes, elementValue);
    // }

    /**
     * Agrega un elemento XML al XML que estamos manejando.
     * 
     * @param elementName
     *            El nombre del elemento.
     * @param anAttribute
     *            Un atributo para incluir en el tag de apertura del elemento.
     * @param elementValue
     *            El valor del elemento.
     * @return true cuando el elemento se pudo agregar correctamente. caso contrario false.
     */
    public boolean addElementAtRoot(String elementName, String anAttribute, String elementValue) {
	String[] attributes = null;
	if (anAttribute != null)
	    attributes = new String[] { anAttribute };
	return this.addElement(ROOT_PATH, elementName, attributes, elementValue);
    }

    /**
     * Agrega un elemento XML al XML que estamos manejando.
     * 
     * @param anElement
     *            Un {@link XMLElement} conteniendo el elemento que se quiere agregar.
     * @return true cuando el elemento se pudo agregar correctamente. Caso contrario false.
     */
    public boolean addElementAtRoot(XMLElement anElement) {
	if (anElement == null)
	    return false;

	return this.addElement(ROOT_PATH, anElement.toString());
    }

    /**
     * Agrega un elemento XML al XML que estamos manejando.
     * 
     * @param path
     *            El path donde se va a agregar el elemento.
     * @param elementName
     *            El nombre del elemento.
     * @param attributes
     *            Un array de {@link String} con los atributos para incluir en el tag de apertura del elemento.
     * @param elementValue
     *            El valor del elemento.
     * @return true cuando el elemento se pudo agregar correctamente. Caso contrario false.
     */
    public boolean addElement(String path, String elementName, String[] attributes, String elementValue) {
	String element = null;
	String rollBackXml = this.xml;
	try {
	    element = this.getCompleteStartTag(elementName, attributes);
	    element += elementValue + this.getEndTag(elementName);

	    int breakPoint = this.indexOfPath(path);
	    this.buffer = this.xml.substring(0, breakPoint);
	    this.buffer += element;
	    this.buffer += this.xml.substring(breakPoint);
	    this.xml = this.buffer;
	}
	catch (Exception e) {
	    Logger.logError(Logger.getLocationMessageFor(this, "addElement(...)"), e);
	    this.xml = rollBackXml;
	    return false;
	}
	return true;
    }

    /**
     * Concatena attributeName y attributeValue de manera tal que quede un {@link String} listo para agregar como
     * atributo de un tag de apertura de un elemento XML.
     * 
     * @param attributeName
     *            El nombre del atributo.
     * @param attributeValue
     *            El valor del atributo.
     * @return El {@link String} ya formado.
     */
    public static String makeAttributeWith(String attributeName, String attributeValue) {
	return attributeName + "=\"" + attributeValue + "\"";
    }

    /**
     * @param path
     *            El path del cual queremos averiguar su indice.
     * @return Un int indicando el indice donde comienza el valor del ultimo elemento del path indicado en path.
     */
    private int indexOfPath(String path) {
	if (path != null) {
	    if (path.equals(ROOT_PATH)) {
		return this.getElementValueIndexes(this.rootTagName)[1];
	    } else {
		return 0;// TODO : Functionality : Terminar indexOfPath(String path) en XMLHandler
	    }
	}
	return 0;
    }

    /**
     * Agrega un elemento XML al XML que estamos manejando, y lo agrega al final del nodo raiz.
     * 
     * @param element
     *            Un string con el elemento ya en formato en XML que queremos agregar.
     * @return true cuando el elemento se pudo agregar correctamente. Caso contrario false.
     */
    public boolean addElementAtRoot(String element) {
	return this.addElement(ROOT_PATH, element);
    }

    /**
     * Agrega un elemento XML al XML que estamos manejando.
     * 
     * @param path
     *            El path donde se va a agregar el elemento.
     * @param xmlElement
     *            Un string con el elemento ya en formato en XML que queremos agregar.
     * @return true cuando el elemento se pudo agregar correctamente. Caso contrario false.
     */
    public boolean addElement(String path, String xmlElement) {
	String rollBackXml = this.xml;
	try {
	    int breakPoint = this.indexOfPath(path);
	    this.buffer = this.xml.substring(0, breakPoint);
	    this.buffer += xmlElement;
	    this.buffer += this.xml.substring(breakPoint);
	    this.xml = this.buffer;
	}
	catch (Exception e) {
	    Logger.logError(Logger.getLocationMessageFor(this, "addElement(...)"), e);
	    e.printStackTrace();
	    this.xml = rollBackXml;
	    return false;
	}
	return true;
    }

    /**
     * Genera el tag XML completo para el tagName especificado, agregando los atributos attributes.
     * 
     * @param tagName
     *            El nombre del elemento XML
     * @param attributes
     *            un array de atributos XML con formato: anAttribute="value1"
     * @return Un {@link String} con formato XML similar a: <tagName anAttribute="value1" otherAttribute="value2">
     */
    private String getCompleteStartTag(String tagName, String[] attributes) {
	String buffer = LT_STRING + tagName;

	if (attributes != null)
	    for (int idx = 0; idx < attributes.length; idx++) {
		buffer += " " + attributes[idx];
	    }

	return buffer + GT_STRING;
    }

    /**
     * Nota: El XML debe contener solo los nodos hijos. Es decir, si el XML es:
     * <padre><hijo1>valor1</hijo1><hijo2>valor2</hijo2></padre> entonces el valor del XML debe ser:
     * <hijo1>valor1</hijo1><hijo2>valor2</hijo2>, o sea, sin el nodo padre.
     * 
     * @return Un int indicando la cantidad de elementos del XML.
     */
    public int getElementCount() {
	XMLHandler handler = new XMLHandler(this.xml);

	String aux = handler.toString();

	int count;
	for (count = 0; aux.length() > 0; count++) {
	    String firstTagName = handler.getFirstElementTagName();

	    String endTag = this.getEndTag(firstTagName);
	    int breakpoint = aux.indexOf(endTag) + endTag.length();

	    aux = aux.substring(breakpoint);
	    handler = new XMLHandler(aux);
	}

	return count;
    }

    /**
     * Elimina el nodo raiz del xml que maneja el objeto this.
     * 
     * @return El xml resultante.
     */
    public String removeRootTag() {
	this.xml = this.getElementValue(this.getFirstElementTagName());
	return this.xml;
    }

    /**
     * @return El {@link String} correspondiente al prefijo que llevan los documentos XML indicando la version y el tipo
     *         de encode que utiliza el XML.
     */
    public String getXmlVersionTag() {
	if (this.xmlVersionTag == null) {
	    int indexOfGreaterThan = this.xml.indexOf(GT_CHAR);
	    if (indexOfGreaterThan > 0)
		return this.xml.substring(0, indexOfGreaterThan + 1);// +1 para que incluya el '>'
	}

	return this.xmlVersionTag;
    }

    public void setXmlVersionTag(String xmlVersionTag) {
	this.xmlVersionTag = xmlVersionTag;
    }

    public String toString() {
	return this.toXMLString();
    }

    public void initializeFromXML(String xmlSource) {
	if (xmlSource != null)
	    this.xml = xmlSource;
	else
	    this.xml = "";
    }

    public String toXMLString() {
	return (this.xmlVersionTag == null ? "" : this.xmlVersionTag) + this.xml;
    }

}
